Repository navigation
fix(compiler): scope single-definition variable inlining to the declaring block - #413
Open
YevheniiKotyrlo wants to merge 1 commit into
Open
YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
This was referenced Aug 14, 2026
YevheniiKotyrlo
marked this pull request as ready for review
August 15, 2026 19:13
Contributor
Author
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 11, 2026 12:35
b77fafb to
dcef09c
Compare
This was referenced Oct 11, 2026
YevheniiKotyrlo
marked this pull request as draft
October 23, 2026 15:52
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 23, 2026 16:38
077d5c6 to
aeb29d6
Compare
…ring block The inliner folded a custom property declared once into every var() in the stylesheet, whether or not the reference's element can hold the declaration. It now folds a value only into the block that declares it, or from a declaration outside @media and @container that reaches every element: `*`, or `:root`, `:host` and `html` for an inheriting property. An @supports block counts as extractRule decides it, through one rulesTheCompilerKeeps, so a dropped block is emptied before the census; a registered `*` property that nothing declares folds to its initial value; and a declaration is removed once no rule reads it. Values the fold no longer normalises reach the token path, which now reads `0` and `false` as values and gives a <ratio> the descriptor parseAspectRatio gives it. A compiled box-shadow drops a transparent shadow, as the runtime does.
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 23, 2026 18:25
aeb29d6 to
2dbd607
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 23, 2026 18:48
This was referenced Oct 28, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
inlineVariablesfolds a custom property declared once into everyvar()that reads it and deletes the declaration, counting declarations with a visitor that cannot see the rule around them, so the fold ignores where the declaration applies. Onmain:.parent { --x: 10px } .child { width: var(--x) }compiles.childtowidth: 10, so an element carrying only.childgets a width Chromium, Firefox and WebKit do not give it (css-variables-1 §2), and a second--xanywhere in the sheet turns the fold off.@supportsblock still declares. Tailwind repeats each@propertyinitial value in an@supportsfallback, which the compiler drops (supportsConditionValid) while the visitor still counts the declaration in it, the value that folds: with@property --x { syntax: "*"; inherits: false; initial-value: 10px }, a fallback* { --x: 20px }and.b { width: var(--x) },mainfolds20, whereinlineVariables: false, Chromium and Firefox render10.inherits: false.:root { --x: 20px }folds20into.balthough the registration says other elements hold the initial value; here the reference is left to the runtime, which fix(native): honourinherits: falseon registered custom properties #412 teaches the registration..a { --c: 0.5; color: rgb(255 0 0 / var(--c)) }loses--c, because the fold does not reach inside an unresolved colour, and the alpha comes out empty.Folding also picks the parser a value meets, and three disagree with the runtime:
0renders nothing unfolded, becauseparseUnparsedread0as an absence;<ratio>keeps the text16 / 9where the property parser gives16/9;Solution
The inliner takes its census from its own walk over the stylesheet and folds a value into a
var()only when every element the reference's rule matches holds it:*, or:root,:hostandhtmlfor a property that inherits, outside@mediaand@container.An
@supportsblock is decided by onerulesTheCompilerKeeps, whichextractRulenow uses too, so a kept block is unconditional and a dropped one is emptied before the census. A registered*-syntax property that nothing declares folds to its initial value (css-properties-values-api-1 §3.3). A declaration is removed once no rule reads it, so one another rule reads at runtime stays.The three parsers now agree with the runtime:
0andfalseare values, a<ratio>getsparseAspectRatio's descriptor, and both paths drop a transparent shadow through oneisTransparentColor.Three existing expectations change:
transparent shadow is preservedpinned the compile-time path keeping it, and both paths drop it now;scale-0andscale-50now fold, so they get the compiler'sscaleXandscaleYfor the runtime'sscale, the same scale; and threevariables.test.tsxcases read"green"and"red"for"#008000"and"#f00", since the colour now arrives by runtime inheritance, which keeps a named colour as written, and React Native reads both.Tests
src/__tests__/compiler/inline-variables.test.ts: where a value folds and where it must not, by selector, conditional rule,@supports, registration, nesting, keyframe and!important, and when a declaration is removed or kept.src/__tests__/native/variable-inlining.test.tsx: what the runtime paints for the same stylesheets.src/__tests__/native/variable-fold-parity.test.tsx: every value kind lightningcss can put in a custom property, rendered written, folded and kept: folding matches the written value for every kind, and a kept variable does except where a row names the runtime's open defect: a named colour stays a name,scaleandrotatekeep their own keys until fix(transforms): resolve and cascade transform properties as browsers do #389, a1px 2pxshorthand stays a list (margin: var(--v)with--v: 1px 2pxreaches native asmargin: [1, 2], which React Native drops #518), and a lonetransition-durationstays unanimated (fix(native): animate a transition or animation held in a variable #497).src/__tests__/native/box-shadow.test.tsx: transparent shadows dropped on both paths, and a transparent list still replacing an earlier one.On
main, 45 of the 209 cases in the six files fail; all pass here, and each of 25 mutations, one per rule, fails a case.The scope cases agree with Chromium 156, Firefox 157 and WebKit 27.2, from
*and:rootunderinherits: falseand an element outside the declaring rule to a variable declaredinitialand a keyframe's--xread by its declaring rule; in the Tailwind-shaped fallback, Chromium and Firefox drop the block as the compiler does, and Playwright's WebKit keeps it.Verification
Every
ci.ymljob passes on this branch merged intomain(2421ea7): lint, typecheck,yarn test(1472 passed),node --test, both builds with nothing unstaged and commitlint on Ubuntu with Node 22.18.0, and the iOS dev build with my Mac's Xcode 27.1, whereci.ymlpins 26.6.Compiling a Tailwind v4 stylesheet of 1,708 classes takes about 480 ms here as on
main(Node 26.12.1, 360 compiles a side): this branch is 1.7% faster, inside the 2.1% two runs of one tree differ by.No existing issue tracks this; I searched
inlineVariables,variable inlining,custom property scope,@property initial-valueand@supports.Device evidence, before and after: #413 (comment)
Open questions
.a { --x: red; color: var(--x) }folds within.a, sovars({ "--x": "blue" })on the element never reachescolor;excludeandinlineVariables: falsekeep it writable. Every fold shares this,:root's and*'s too, as the compiler cannot know what an app writes at runtime: keep same-block folds, or leave them to the runtime?Related
References
mainfolded across blocks now reach the runtime and its open defects: a shorthand read through a variable reaches React Native as one list (#518), atransition-*longhand read through one stays unanimated (#497), and on lightningcss 1.31 or later avar()left in the sheet makescompile()throw (#470). #412 makes the runtime honourinherits: false.This conflicts with #379, #389, #391, #411, #412, #417, #420, #489, #491, #494, #495, #497, #501 and #503; I'll re-cut whichever lands after the others.